public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");

        // **** 常见的锁策略 **** (用八股的方式来看待)
        // 1. 乐观锁 / 悲观锁
        // 站在锁冲突概率的预测的角度
        // 悲观锁: (锁冲突概率较高)
        //总是假设最坏的情况，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这
        //样别人想拿这个数据就会阻塞直到它拿到锁。
        //乐观锁：(锁冲突概率不高)
        //假设数据一般情况下不会产生并发冲突，所以在数据进行提交更新的时候，才会正式对数据是否产生并
        //发冲突进行检测，如果发现并发冲突了，则让返回用户错误的信息，让用户决定如何去做。
        // Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.


        // 2.自旋锁 / 挂起等待锁

        // 3. 普通互斥锁 / 读写锁
        // 多线程之间，数据的读取方之间不会产生线程安全问题，但数据的写入方互相之间以及和读者之间都需
        //要进行互斥。如果两种场景下都用同一个锁，就会产生极大的性能损耗。所以读写锁因此而产生。
        //读写锁（readers-writer lock），看英文可以顾名思义，在执行加锁操作时需要额外表明读写意图，复
        //数读者之间并不互斥，而写者则要求与任何人互斥

        //一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
        //两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
        //两个线程都要写一个数据,  有线程安全问题.
        //一个线程读另外一个线程写, 也有线程安全问题

        // 读写锁就是把读操作和写操作区分对待. Java 标准库提供了ReentrantReadWriteLock 类, 实现了读写锁.
        //ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行
        //加锁解锁.
        //ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进
        //行加锁解锁.
        //其中,
        //读加锁和读加锁之间, 不互斥.
        //写加锁和写加锁之间, 互斥.
        //读加锁和写加锁之间, 互斥.


        // 4. 重量级锁 vs 轻量级锁
        // 锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
        //CPU 提供了 "原子操作指令".
        //操作系统基于 CPU 的原子指令, 实现了
        //mutex 互斥锁.
        //JVM 基于操作系统提供的互斥锁, 实现了
        //synchronized 和
        //ReentrantLock 等关键字和类.

        // 5.公平锁 / 非公平锁

        // 假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后
        //C 也尝试获取锁, C 也获取失败, 也阻塞等待.
        //当线程 A 释放锁的时候, 会发生啥呢?
        // 公平锁: 遵守 "先来后到". B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
        //非公平锁: 不遵守 "先来后到". B 和 C 都有可能获取到锁.

        // 这就好比一群男生追同一个女神. 当女神和前任分手之后, 先来追女神的男生上位, 这就是公平锁;
        //如果是女神不按先后顺序挑一个自己看的顺眼的, 就是非公平锁.

        // 操作系统和 java synchronized 原生都是"非公平锁"
        // 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要
        //想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
        //公平锁和非公平锁没有好坏之分, 关键还是看适用场景.


        // 6. 可重入锁 vs 不可重入锁
        // 可重入锁的字面意思是“可以重新进入的锁”，即允许同一个线程多次获取同一把锁。
        //比如一个递归函数里有加锁操作，递归过程中这个锁会阻塞自己吗？如果不会，那么这个锁就是可重入
        //锁（因为这个原因可重入锁也叫做递归锁）。
        //Java里只要以Reentrant开头命名的锁都是可重入锁，而且JDK提供的所有现成的Lock实现类，包括
        //synchronized关键字锁都是可重入的。
        //而 Linux 系统提供的 mutex 是不可重入锁.
        // 可重入锁: 一个线程针对一把锁,连续加锁多次都不会死锁
        // 不可重入锁: 一个线程针对一把锁,连续加锁多次,会出现死锁

        // synchronized
        // 1.synchronized 既是一个悲观锁 又是一个乐观锁 , 默认是乐观锁 , 但是如果发现当前锁竞争比较激烈,就会变成悲观锁
        // 2.Synchronized 即使重量级锁 又是轻量级锁 , 默认是轻量级锁 , 如果发现当前锁竞争比较激烈 就会转换成重量级锁
        // 3. Synchronized 这里的轻量级锁 就是基于自旋锁的方式实现的, 这里的重量级锁 是基于挂起等待锁的方式实现的
        // 4.Synchronized 不是读写锁
        // 5.Synchronized 是非公平锁
        // 6. Synchronized 是可重入锁
        // 上述谈到的六种策略 可以视为"锁的形容词"

        // **** CAS ****
        // 什么是 CAS (拿着 内存 和 寄存器 进行比较和交换)
        // CAS: 全称Compare and swap，字面意思:”比较并交换“，一个 CAS 涉及到以下操作：
        // 我们假设内存中的原数据V，旧的预期值A，需要修改的新值B。
        //1. 比较 A 与 V 是否相等。（比较）
        //2. 如果比较相等，将 B 写入 V。（交换）
        //3. 返回操作是否成功。
        // 在上述交换的过程中,大多数并不关心,B后续的情况了,更关心的是V这个变量的情况,这里说是交换,也可以近似理解成"赋值"
        // 如果 V 和 A 值不同 , 则无事发生

        // 那么此处最特别的地方, 上述这个CAS的过程,并非是通过一段代码实现的,而是通过 一条CPU特定的"原子的"指令 完成的
        //  一条指令 则就说明是 上述的步骤是原子性的 ,CAS操作是原子的 (就可以在一定程度上回避线程完全问题)
        // 咱们解决线程安全问题方案除了加锁之外,又有了新的方向
        // 小结:CAS 可以理解成CPU 给咱们提供的一个特殊指令,通过这个指令 就可以一定程度的处理线程安全问题

        // **** CAS 的应用场景 *****
        // 1. 基于CAS 实现原子类 (这个原子类 也是Java标准库提供的类,,就是在进行自增自减的时候是原子操作)
        //代码实现 (原子类这里的实现,每次修改之前,再确认一下这个值是否符合要求)
        // 2. 基于CAS 实现自旋锁
        // 自旋锁伪代码 (假的代码(不能编译运行,只是表达了一个大概的逻辑))
        /*  public class SpinLock {
         private Thread owner = null;  // 表示 当前的锁是谁加的
         public void lock(){
         // 通过 CAS 看当前锁是否被某个线程持有.
        // 如果这个锁已经被别的线程持有, 那么就自旋等待.
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
        while(!CAS(this.owner, null, Thread.currentThread())){ // 检测当前的owner是不是null
        // 是null 就进行交换,也就是把当前线程的引用赋值给 owner 如果赋值成功,此时循环结束,加锁完成了
        // 如果当前锁,已经被别的线程占用了,CAS 就会发现 this.owner 不是null,CAS就不会产生赋值,也同时返回false,循环继续执行,并进行下次判定...

                }
            }
         }
         public void unlock (){
         this.owner = null;  // 检索
            }*/

        // 基于CAS, 确实是更高效的解决了线程安全问题,但是CAS不能代替锁.CAS试用范围是有限的. 不像锁使用范围那么广

        //CAS的典型问题:ABA问题
        // CAS 在运行中的核心,检查 value 和 oldValue 是否一致,如果一致,就是为 value 中途没有被修改过,所以进行下一步交换操作是没有问题的
        // 这里的一致,可能没改过 也可能是改过但是还原回来了?
        //  ABA 的问题: (cas 判定 value 为 A ,此时可能确实 value 始终是 A,也可能是 value 本来是 A ,被改成B ,又还原成A)
        //假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
        //接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
        //先读取 num 的值, 记录到 oldNum 变量中.
        //使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
        //但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A
        // 线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这
        //个时候 t1 究竟是否要更新 num 的值为 Z 呢?
        // 到这一步, t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程.

        // ABA 这个情况 , 大部分情况下,其实是不会对代码/逻辑产生太大影响的
        // 但是不排除一些"极端情况" , 也是可能造成影响的
        // 什么是极端情况 ? 不好实现,可以脑补出一个
        // (实际开发中概率是非常低的)

        // 假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 在按下取款的瞬间,机器卡了一下,滑稽老铁就忍不住多按了一下,
        // 这时取款机创建了两个线程, 并发的来执行 -50操作.
        //我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
        // 但是这时候可能是 一个线程扣款成功 余额变成50 , 而另一个线程在进行CAS操作的时候 100 不等于 50 所以就不执行,扣款失败
        // 但! 假设在这之间,又有人给滑稽转账了50 , 此时这个线程t3 的操作,让滑稽老铁的账单就变成了100
        // 这时候第二次的CAS操作就执行成功了
        // 这时候扣款就出现bug了

        //为了解决 ABA 问题, 给余额搭配一个版本号, 初始设为 1.
        //1) 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100,
        //版本号为 1, 期望更新为 50.
        // 2) 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
        //3) 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.
        //4) 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读
        //到的版本号为 1, 版本小于当前版本, 认为操作失败.

        // 基于版本号的方式 也是乐观锁的一种典型方式

        // 在 Java 标准库中提供了
        //AtomicStampedReference<E> 类. 这个类可以对某个类进行包装, 在内部就提
        //供了上面描述的版本管理功能.
        //关于
        //AtomicStampedReference<E> 的具体用法此处不再展开. 有需要的同学自行查找文档了解
        //使用方法即可.

        // 相关面试题
        //1) 讲解下你自己理解的 CAS 机制
        //全称 Compare and swap, 即 "比较并交换". 相当于通过一个原子的操作, 同时完成 "读取内存, 比
        //较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的支撑.
        //2) ABA问题怎么解决？
        //给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
        //如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当
        //前版本号比之前读到的版本号大, 就认为操作失败.

        // **** Synchronized 原理 ****
        // 两个线程针对同一个对象加锁就会产生阻塞等待
        // synchronized 内部还有一些优化机制,存在的目的就是让锁更加高效

        // 1. 锁升级/锁膨胀
        // 会有一下四种状态
        // 1) 无锁
        // 2) 偏向锁
        // 3) 轻量级锁
        // 4) 重量级锁

        // 当代码执行到加锁的代码块之后 , 会从 无锁状态变成 偏向锁状态 (并不是真正的加锁,而只是占个位置.有需要再真加锁,没需要就算了) 做个标记,(这个过程是轻量的)
        // 如果整个使用锁过程中,都没有出现锁竞争,在 synchronized 执行完之后,取消偏向锁即可
        // 但是,如果使用的过程中,另一个线程也尝试加锁,在它加锁之前,就迅速的把偏向锁升级成真正的加锁状态,另一个线程也就只能阻塞等待了
        // 当 synchronized 发生锁竞争的时候 就会从偏向锁,升级成轻量级锁
        // 此时 synchronized 相当于通过自旋的方式,来进行加锁的
        // (如果要是很快别人就释放锁了,自旋是很划算的,但是如果迟迟拿不到做,就会一直自旋,并不划算)
        // (但是 synchronized 自旋并不是无休止的自旋,自旋到一定程度之后,就会再次升级 成 重量级锁(挂起等待锁) (这个步骤则是 基于操作系统原生的API来进行加锁了))
        // (此时,如果线程进行重量级加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中.暂时不考虑CPU调度了.然后直到锁被释放了,这个线程才有机会被调度到,并且有机会获取锁)
        // 一旦当前线程被切换出cpu,这就是个比较低效的事情了,(从cpu再切换回来 呢就是毫秒级别的事情了)

        // 当前的jvm主流的实现中,只有锁升级,没有锁降级,被升级了就回不了头了

        // 上述锁升级的过程, 都是 实现JVM的大佬们, 可怜咱们这些菜鸟程序员, 怕咱们代码写的太烂,太低效
        // 引入的一系列优化, 保证程序员在无感知的情况下提升代码的效率
        // 但是万万没想到,初心适得其反~~,虽然咱们在写代码的时候,代码不受到影响,执行效率高了,但是给面试增加了不少的麻烦


        // 2.锁消除
        // 编译器智能判定,看当前的代码是否真的要加锁,
        // 如果这个场景不需要加锁,程序员也加了 , 就自动给程序员干掉
        // 锁消除
        //编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
        // 有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)
        //  StringBuffer sb = new StringBuffer();
        // sb.append("a");
        // sb.append("b");
        // sb.append("c");
        // sb.append("d");
        // 此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加
        //锁解锁操作是没有必要的, 白白浪费了一些资源开销.

        // 3.锁粗化
        // 一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.
        // 锁的粒度: 粗和细
        // (被synchronized 包含的代码越多 粒度越粗, 包含的代码越小,粒度越细)
        //(通常情况下 认为锁的粒度细一点比较好,加锁部分的代码,是不能并发执行的.锁的粒度越细,能并发的代码就越多;反之就越少)
        // 实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.
        //但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.
        //  滑稽老哥当了领导, 给下属交代工作任务:
        //方式一:
        //打电话, 交代任务1, 挂电话.
        //打电话, 交代任务2, 挂电话.
        //打电话, 交代任务3, 挂电话.
        //方式二:
        //打电话, 交代任务1, 任务2, 任务3, 挂电话.
        //显然, 方式二是更高效的方案
        // 可以看到, synchronized 的策略是比价复杂的, 在背后做了很多事情, 目的为了让程序猿哪怕啥都不懂,
        //也不至于写出特别慢的程序

        // 相关面试题
        // (上面锁升级的过程)
        //1) 什么是偏向锁?
        //偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线
        //程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞
        //争, 再取消偏向锁状态, 进入轻量级锁状态.

        //  synchronized 实现原理 是什么？



        //**** Callable 接口 *****
        // 类似于 Runnable 一样 (Runnable 是用来描述一个任务)
        // Callable 也是描述一个任务
        // 只不过 Runnable 是没有返回值  Callable 有返回值, 如果 需要使用一个线程单独计算结果 , 那就需要 Callable

        //***** JUC(java.util.concurrent) 的常见类 *****
        // 这个包中放了很多并发编程(多线程)相关的组件 , 并发编程是一个更广义的概念, 多线程 是实现并发编程的一种方式, 当然还有其他方式实现并发编程
        // 1. ReentrantLock  (一定要会念会读)
        // 可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
        // ReentrantLock 也是可重入锁. "Reentrant" 这个单词的原意就是 "可重入"

        // 为什么有了 synchronized 还要有 ReentrantLock 呢?

        // (synchronized 是直接基于代码块的方式来加锁解锁的 . ReentrantLock 更传统, 使用了 lock 方法 和 unlock方法加锁解锁)
        //  ReentrantLock 的用法:
        // lock(): 加锁, 如果获取不到锁就死等.
        // trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
        // unlock(): 解锁

        // ReentrantLock 和 synchronized 的区别:
        // synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准
        //库的一个类, 在 JVM 外实现的(基于 Java 实现).
        //synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
        //但是也容易遗漏 unlock.
        //synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就
        //放弃.
        //synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启
        //公平锁模式.

        // **** 信号量Semaphore ****
        // 信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.
        // 理解信号量
        //可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
        //当有车开进去的时候, 就相当于申请一个可用资源, 可用车位(计数器)就 -1 (这个称为信号量的 P 操作)
        //当有车开出来的时候, 就相当于释放一个可用资源, 可用车位(计数器)就 +1 (这个称为信号量的 V 操作)
        //如果计数器的值已经为 0 了, 还尝试申请资源(P操作), 就会阻塞等待, 直到有其他线程释放资源.
        // Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用. (P V 是 荷兰语)
        // (英文中 P 是 acquire 申请  V 是 release 释放 )

        // 当 计数初始值 是 1 的信号量 , 针对这个信号量的 值 就只有 1 和 0 这两种值 (信号量不能为空)
        // 执行一次P操作 1 -> 0
        // 执行一次V操作 0 -> 1

        // 如果已经进行了一次P操作,再继续进行P操作就会阻塞等待
        // 这种情况呢 就和 锁 一样了 (锁就是 计数器 为 1  的信号量, 二元信号量)
        // 所示信号量的一种特殊情况
        // 信号量就是锁的一般表达

        // 但是 锁 用的是最多的,但是信号量也是是偶尔用到的

        // ****  CountDownLatch **** (简单了解)
        // 同时等待 N 个任务执行结束.  (用的不是特别多, 特定场景使用的小工具)
        // 好像跑步比赛，10个选手依次就位，哨声响才同时出发；所有选手都通过终点，才能公布成绩。
        // (主要是两个方法)
        // 1. await(wait 是等待 a => all) 主线程来调用这个方法,此处的a单值all的意思, 然而a前缀的,大多数表示"异步"
        // (同步和异步是相对的: 简单解释一下,同步这个词有多个含义,(计算机里面有很多术语都是一词多用的), 在IO场景中,也会有 同步异步 的概念)
        // (同步就是: 发送方请求发送方做事, 异步就是 发送方请求,接收方做事 , 大概介绍一下)

        // 2. countDown 再构造的时候 指定一个计数(选手个数)
        // (例如有四个选手参加,初始情况下,调用 await 就会阻塞, 每个选手都冲到终点,都会调用countDown方法)
        // (前三次调用 countDown , await 没有什么影响)
        // (第四次调用countDown , await 就会被唤醒, 返回(解除阻塞), 此时就可以认为比赛结束了)

        // 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
        //每个任务执行完毕, 都调用latch.countDown()  . 在 CountDownLatch 内部的计数器同时自减.
        //主线程中使用latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
        // CountDownLatch 也是有很多使用的场景
        // 在一个下载器中 下载一个视频文件 , 好几个G , 多线程下载就会把一个大文件 切分成好几个小块的文件,安排多个线程充分下载
        // 此处就可以使用 CountDownLatch 来进行区分是不是整体下载完了


        // 相关面试题
        //1) 线程同步的方式有哪些？
        //synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.
        //2) 为什么有了 synchronized 还需要 juc 下的 lock？
        //以 juc 的 ReentrantLock 为例,
        //synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
        // synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
        //synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式.
        //synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的
        //线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
        // 3) AtomicInteger 的实现原理是什么？
        // 执行过程参考 "CAS 有哪些应用" 章节.
        // 4) 信号量听说过么？之前都用在过哪些场景下？
        //信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.
        //使用信号量可以实现 "共享锁", 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为
        //加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,
        //直到前面的线程执行了 V 操作.
        //5) 解释一下 ThreadPoolExecutor 构造方法的参数的含义
        //参考上面的 ThreadPoolExecutor 章节

        // **** 线程安全的集合类 ****
        // Java 标准库里很多的集合类都是线程不安全的
        // Vector, Stack, HashTable, 是线程安全的(不建议用)(因为很多场景是不需要加锁的), 其他的集合类不是线程安全的.

        // 多线程环境使用 ArrayList
        // 1) 自己使用同步机制 (synchronized 或者 ReentrantLock) [常见]
        // 2)Collections.synchronizedList(new ArrayList); (使用这个方法把集合类套一层 , 这里会提供一些 ArrayList 相关的方法 , 同时是带锁的)
        // 3) 使用 CopyOnWriteArrayList [COW , 也叫做 写时拷贝]
        // CopyOnWrite容器即 写时复制的容器。
        //当我们往一个容器添加元素的时候，不直接往当前容器添加，而是先将当前容器进行Copy，
        //复制出一个新的容器，然后新的容器里添加元素，
        //添加完元素之后，再将原容器的引用指向新的容器。
        //这样做的好处是我们可以对CopyOnWrite容器进行并发的读，而不需要加锁，因为当前容器不会添加任何元素。
        //所以CopyOnWrite容器也是一种读写分离的思想，读和写不同的容器。
        // 优点:
        //在读多写少的场景下, 性能很高, 不需要加锁竞争. (比如在 服务器程序的 配置 维护~ )
        //缺点:
        //1. 占用内存较多.
        //2. 新写的数据不能被第一时间读取到.

        // 多线程环境使用队列
        // 1) ArrayBlockingQueue
        // 2) LinkedBlockingQueue
        // 3) PriorityBlockingQueue
        // 4) TransferQueue 最多只包含一个元素的阻塞队列

        //多线程环境使用哈希表 (重点)
        // HashMap 本身不是线程安全的.
        // 在多线程环境下使用哈希表可以使用:
        // HashTable (给关键方法加了 synchronized)
        // ConcurrentHashMap (这个时更推荐使用的 , 更优化的线程安全哈希表) (考点)

        // ConcurrentHashMap 进行的哪些优化 ?  或者 比 HashTable 好在哪里? 和 HashTable 之间的区别是啥 ?
        // 1) (最大的优化之处) ConcurrentHashMap 相比于 HashTable 大大缩小了锁冲突概率, 把一把大锁, 转换成多把小锁
        // (HashTable 做法是直接在方法上加上 synchronized 等于是给 this 加锁. 只要操作哈希表上的任意元素, 都会产生加锁, 也就都有可能会发生锁冲突)
        // (但实际上 , 仔细思考不难发现 , 其实基于 哈希表 的结构特点 有些元素在进行并发操作的时候 , 是不会产生线程安全问题的, 也就不需要使用锁控制)
        // (哈希表 是 由 数组 + 链表(也可能是二叉树)形式组成)
        // 而ConcurrentHashMap 做法是 每个链表有各自的锁 , 而不是大家公用同一个锁 , (用每一个链表的头节点 作为 锁对象)
        // 必要的时候加锁才加锁 , 不必要的时候就不加锁 , 上面的情况是针对JDK1.8以后的情况
        // 但是在JDK 1.7 之前使用的是"分段锁" ,(把一段合成一把锁)
        // 2) ConcurrentHashMap 做了一些激进的操作, 针对读操作不加锁 , 只针对写进行加锁
        // 读和读 之间没有冲突
        // 写和写 之间有冲突
        // 读和写 之间也没有冲突....(很多场景下,如果写操作不是原子的, 就会出现"脏读"的情况) (当然ConcurrentHashMap 写操作是原子的)
        // 3)ConcurrentHashMap 内部充分的使用了CAS,通过这个也来进一步的削减加锁操作的数目(比如维护元素个数)

        // 4) 针对扩容, 采取了"化整为零"的方式(HashMap/ HashTable 扩容: 创建一个更大的数组空间, 把旧的数组上的链表的每个元素搬运到新的数组上.(删除+插入)
        //  这个扩容操作会在某次put的时候进行触发. 如果元素个数特别多, 就会导致这样的搬运操作, 比较耗时, 会出现某次 put 比 平时的 put卡很多倍)
        // ConcurrentHashMap 的扩容 是采取每次搬运一小部分的元素的方式 , 创建新的数组, 旧的数组也保留. 每次 put操作,都往新数组上添加, 同时进行一部分搬运(把一小部分旧的元素搬运到新数组上)
        // 每次get的时候 , 则旧数组和新数组都查询
        // 每次remove的时候,只是把元素删了就行
        // 经过一段时间后,所有的元素都搬运好了,最终再释放旧数组


        // 相关面试题
        //1) ConcurrentHashMap的读是否要加锁，为什么?
        //读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了volatile 关键字.

        //2) 介绍下 ConcurrentHashMap的锁分段技术?
        //这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
        //目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.

        //3) ConcurrentHashMap在jdk1.8做了哪些优化？
        //取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).
        //将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树.

        //4) Hashtable和HashMap、ConcurrentHashMap 之间的区别?
        //HashMap: 线程不安全. key 允许为 null
        // Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
        //ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用
        //CAS 机制. 优化了扩容方式. key 不允许为 null
        // 还是说上面的呢4点才是最重要的


    }
}